Apr 86 Tech Questions
Volume Number: 2
Issue Number: 4
Column Tag: Ask Professor Mac
Reader's Technical Questions
By Steve Brecher, Software Supply, 4618 E. Sixth St., Long Beach, CA.
90814
Steve only writes when someone sends him an interesting question to answer.
Help us keep Steve busy by sending in your technical questions to Professor Mac. You
may write to Steve directly at his software store listed above.
Desk Accessories and JIODone
Q. Jan Eugenides sent me a question about how the Control routine of a desk
accessory should return: via an RTS instruction or via a JMP through JIODone? This
turns out to be a somewhat complex issue; the following discussion is based on answers
from my most trusted authority, the ROM, and insights provided by Lew Rollins and
Jon Hueras via the CompuServe Mac Developers SIG.
A. Inside Macintosh says that the Control routine of a driver should return to
the ROM's IODone routine -- to the address contained in the system global variable
JIODone. When jumping through JIODone, register A1 must contain the address of the
driver's Device Control Entry (DCE); otherwise, no registers need to be preserved by
the Control routine. Generally, any queued request to a driver must return to IODone
so that the system won't hang waiting for completion of the request.
If a driver doesn't need to be locked in the heap when it is not executing, it has
the dRAMBased bit in the DCE set and the dNeedLock bit clear. Note that a driver is
always locked before it is entered, so self-locking by a driver is superfluous. The
dNeedLock bit specifies whether the driver needs to be locked when it is not executing.
If dNeedLock is clear, IODone will unlock the driver and the DCE.
For queued I/O requests, IODone removes the completed request from the queue
and dequeues the next pending request, if any, for execution. However, Desk Accessory
(DA) Control requests are not queued (they have the noQueue bit set in the Conrol trap
word) except for the case of the "goodbye kiss" (csCode = -1) call that is issued if the
dNeedGoodbye bit is set in the driver header.
But... the IODone routine in the 64K ROM has a bug (fixed in the 128K ROM). In
order to determine if the request was a queued one, it looks at the noQueue (,IMMED)
bit in the trap word in the queue element pointed to by the dCtlQueue field of the DCE.
Problem is, if the request was not a queued one, then it's not in the queue to be
examined! Since DA Control calls (except for a "goodbye kiss") are not queued, IODone
will be using a Nil pointer in the dCtlQueue field and will examine the word at offset
ioTrap (offset 6) from address zero, i.e., address 00000006. As luck would have it,
usually the noQueue bit happens to be set at that irrelevant location, and IODone
doesnt't try to remove the nonexistent queue element - so the bug doesn't usually bite.
But it's dicey for a DA to depend on that happenstance.
A further complication for some DAs is that their Control routines may be
reentrant. Consider the case of a DA that has the dNeedTime bit set in its header because
it wants to be called periodically. Suppose this DA's Control routine calls ModalDialog
(possibly indirectly via SFGet/PutFile or Alert). ModalDialog calls SystemTask, which
in turn may issue a periodic Control call to the DA. If the DA is always unlocked at
Control exit (either explicitly by itself or by IODone), then the periodic call will at
exit unlock the DA/DCE, and when ModalDialog returns the hapless DA will find itself
"mysteriously" unlocked with perhaps disastrous results. If you share Jon Heuras's
"sheer paranoia" about reentrancy, you can use his technique of clearing the dCtlEnable
bit in the DCE at entrance to Control, and setting it again at exit -- this avoids
reentrant Control calls.
Ok, so those are the facts -- what does it all mean? I would suggest the following
approach. It assumes that the DA does not need itself nor its DCE to be locked while the
DA is not executing; and it permits reentrance. There are few cases when a DA must be
locked when not executing, and it is impolite to the host application to have
unnecessarily-locked blocks in the heap.
Allocate a word -- or to be safe, a longword -- in the DAs private storage to be
used as a Control entrance counter. Clear this counter in the Open routine. At entrance
to the Control routine, increment the counter. Then the Control exit logic would look
like this:
;A0 points to the request parameter block
;A1 points to the DCE
;the following 4 lines can be omitted if dNeedGoodbye is clear
;- otherwise we assume private storage has beed disposed of
Btst #noQueueBit-8,ioTrap(A0) ;queued request?
Beq.S @0 ;no
Move.L JIODone,A0 ;yes, it's "Goodbye"...
Jmp (A0) ;so return to IODone
@0 Move.L dCtlStorage(A1),A0 ;get storage handle
Move.L (A0),A2 ;dereference
SubQ.L #1,EntranceCount(A2) ;decrement counter
Bne.S @1 ;br if reentered
_HUnlock ;unlock storage
Move.L A1,A0 ;DCE pointer
_RecoverHandle ;DCE handle
_HUnlock ;unlock DCE
Lea Driver,A0 ;addr of this DRVR
_RecoverHandle ;handle to ourself
_HUnlock ;unlock ourself
@1 Rts ;bypass IODone
Scamble Well, Please
Q. Dr. David T. Linker of Trondheim, Norway, writes, “I was encouraged by
your comment on 'dumb' questions [I solicited them! --SB]. Why do Macintosh
debuggers offer to scramble the heap? I use the heap ... but I can't think of a reason
why I would want to scramble it. It seems that this would be undesireable, kind of like
'scramble variables.'
A. "Rearrange" would be a more accurate word for the function offered by the
debuggers. Scrambling the heap means to move relocateable blocks around; if your code
at some point depends on the incorrect assumption that a given block is locked (won't
move), then scrambling the heap may help to reveal that bug. After the scramble, the
program won't find what it expects to find at what it thinks is the address of the block,
and will probably behave differently than it would without the scrambling.
Such a buggy program may have seemed to be healthy only because, by luck, the
block has not moved; but in some circumstances that testing has not yet encountered,
the block will move, and the program will crash or otherwise misbehave. If your
program can survive a heap scramble on any trap which might rearrange the heap,
then you can have some confidence that it is not making incorrect assumptions about
blocks being immobile.
Application Icon
Q. This and the following question were submitted by L. Tannenbaum of Long
Beach, Calif. "How do I get the Finder to use the 'generic' icon for an application? What
info do I include or exclude in the resource file?
A. This is an unusual question - most often, people want to know how to give
their application a custom icon. To get the generic "hand-on-diamond" icon on the
Finder desktop, all you need do is specify APPL for the type of the application file.
Begin your RMaker input file with:
Name Of My Application File
APPLMYAP
This examples uses MYAP as the signature of the application.
Moving Pictures
Q. How do I get a "PICT," say one I drew in MacPaint, into a resource file? I
have RMaker (with no documentation) and REdit (not ResEdit).
A. Use RMaker to create a dummy PICT resource in the target resource file:
TYPE PICT = GNRL ;; define a "new" type via GNRL
,128 ;; resource ID
.I ;; integers follow
0 0 0 0 ;; dummy rectangle coordinates
In MacPaint, cut your picture and paste it into the Scrapbook. Now run REdit and
open your resource file and the PICT resource type. You'll see PICT 128 represented
by an icon. Open the Scrapbook, cut the picture from it, select the PICT 128 icon, and
paste. (P.S. ResEdit is more powerful than REdit; ResEdit lets you create resources as
well as modify them. ResEdit 1.0d5 is available on the MacTutor Utility disk and source
code disk #6. ]
AutoWatch
It's a service to the user for an application to display a wristwatch cursor
whenever the application is busy and not in the mood for user input. But it's something
of a nuisance for the programmer to explicitly change the cursor before and after each
time-consuming operation; besides, sometimes it's not easy to know a priori that an
operation will be time-consuming.
I saw a message from Larry Rosenstein of Apple on one of the networks in which
he outlined a scheme for automating changing of the cursor to a watch and back. The
basic idea is to install both a vertical-blanking (VBL) interrupt task and a hook into
GetNextEvent. If GetNextEvent has not been called for some pre-defined period of time,
then the application is deemed to be "busy" and the cursor is changed to a watch; this is
done by the VBL task.
The system's VBL interrupt handler checks each element in a VBL queue each
60th of a second, decrementing a counter value in the queue element. If the counter
decrements to zero, a task associated with that VBL queue element is executed. Then --
unless the VBL task reinitialized the counter to a non-zero value -- the element is
removed from the queue. For more information, see the Vertical Retrace Manager
chapter of Inside Macintosh.
Suppose we want a watch cursor to be displayed whenever half a second passes
with no calls to GetNextEvent. What we do is install a VBL task with a value of 30
(30/60 of a second) in the associated VBL queue element counter field. We also
revector the GetNextEvent trap to a piece of code which restores the counter in the VBL
queue element to 30 (and then does normal GetNextEvent processing). If GetNextEvent
is called at least once every half second, the VBL task will never be executed, because
the VBL queue element's counter will never get decremented to zero. But if half a
second elapses with no calls to GetNextEvent, the VBL task will be executed.
When the VBL task is executed, it examines a global flag which indicates whether
the cursor has already been changed to a watch. If the flag is false, the task saves the
current cursor, changes the screen cursor to a watch, and sets the flag. Then it
restores the counter field in the VBL queue element so that the element will not be
dequeued.
The GetNextEvent hook code restores the VBL task's timer value, and, if the VBL
task changed the cursor from a non-watch to a watch (indicated by the global flag set by
the VBL task), restores the original cursor.
In simplest terms, every time GetNextEvent is called it says to the Vertical
Retrace Manager, "Whoa! The application is not busy! Restart your countdown." The
Vertical Retrace Manager, each 60th of a second, decrements the counter; if it has
reached zero, that means GetNextEvent has not been called since the countdown last
started, implying a busy application which needs a watch cursor: the VBL task is
executed and changes the cursor.
There are a couple of minor complications. The GetNextEvent hook code cannot
just do an InitCursor (which makes the cursor an arrow), because the pre-watch
cursor may not have been an arrow. But neither can it blindly restore whatever
cursor the VBL task saved. Consider this example: the application calls DIBadMount to
initialize a disk. DIBadMount changes the cursor to a watch as it starts to format the
disk. Shortly thereafter, the VBL task countdown reaches zero, and the VBL task saves
the current cursor (a watch!) and sets the cursor to a watch - superfluous, but no
harm done. When the disk format completes, DIBadMount does an InitCursor, and
returns to the application. The application calls GetNextEvent. Now our GetNextEvent
hook sees the flag set by the VBL task indicating that the VBL task saved the old cursor
and changed the cursor to a watch; so GetNextEvent restores the old cursor. No good!
We've restored to a watch when we don't want a watch. To avoid this, the GetNextEvent
hook looks at the cursor saved by the VBL task; if it's a watch, it doesn't restore it.
Also, there are times when GetNextEvent is not called, but nonetheless it would
be inappropriate to change the cursor to a watch: when the mouse is being tracked by,
e.g., the Menu Manager or the Control Manager. If the user dawdles while he has a menu
pulled down, we can't change the cursor just because GetNextEvent is not being called.
So, the VBL task will not change the cursor if the mouse button is down.
The time period used to initialize the VBL counter is application-dependent.
About half a second works pretty well with an application I recently implemented. If
the period is too short, the cursor will too-often toggle to a watch and back in a
distracting way. If the period is too long, then the user will not be promptly informed
that the application is busy, and the change of the cursor to a watch will seem unrelated
to the preceding user action which initiated the time-consuming process.
My implementation of AutoWatch in MDS Assembler is shown in Figure 1. This
implementation will not work under Switcher.
Move Low
The 128K ROM has a new feature with regard to the loading of CODE segments. If
a CODE resource does not have the resLocked attribute, then the Segment Loader will
load that segment as high as possible in the heap. The idea is that up at or near the top
of the heap, it will be "out of the way" and not contribute to heap fragmentation.
However, this can be a problem with CODE 1, the code segment which is loaded
first and which is usually the main segment of the application. Upon loading, the
Segment Loader will lock it, regardless of the resLocked attribute, as it does for all
segments. It will remain locked until the applicaton unloads it - but most applications
don't unload CODE 1. If CODE 1 does not have the resLocked attribute (which it will not
if generated by, e.g., the Consulair Linker), it will be loaded high in the heap - the
initial heap before any heap expansion has occurred. Since almost all applications
expand the heap either explicitly or implicitly, the new Segment Loader feature is
effectively a "load in middle" for CODE 1.